/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.validator.internal;

import java.beans.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
import ch.simpleel.util.*;
import ch.trackedbean.business.*;
import ch.trackedbean.copier.annotations.*;
import ch.trackedbean.validator.*;
import ch.trackedbean.validator.annotations.*;
import ch.trackedbean.validator.businessValidator.*;
import ch.trackedbean.validator.internal.ExternalAnnotationMapping.ExternalConfigurationException;

/**
 * Helper object for building an {@link Validator} based on {@link BeanPropertyValidator} annotations and mapping annotations ({@link SimpleMapping},
 * {@link PathMapping} etc.).
 * 
 * @author M. Hautle
 */
public class DefaultValidatorConfigurator {
    /** The validator to build. */
    protected final Validator validator;

    /** The type for which the validator is built. */
    protected final Class<?> type;

    /** The base class specified in {@link SourceClass} (if the validator inheritance is enabled) or null. */
    protected final Class<?> mappedBaseType;

    /** Configuration holder. This holder will be reused for each property validator configuration. */
    protected final Configuration cfg = new Configuration();

    /** Set holding the property names of properties were no default mapping validator has to be registred. */
    protected final Set<String> noDefaultMappingValidation = new HashSet<String>();

    /**
     * Default constructor.
     * 
     * @param type The type for which the validator is built
     */
    public DefaultValidatorConfigurator(Class<?> type) {
        this.validator = new Validator(type);
        this.type = type;
        mappedBaseType = getMappedBaseClass(type);
    }

    /**
     * Returns the mapped base class for validation inheritance.
     * 
     * @param type The type
     * @return The mapped base class
     */
    protected Class<?> getMappedBaseClass(Class<?> type) {
        final SourceClass src = type.getAnnotation(SourceClass.class);
        // no mapped class or at least no validation inheritance desired
        if (src == null || type.isAnnotationPresent(IgnoreInheritedValidation.class))
            return null;
        final Class<?> base = src.value();
        return ValidatorManager.getValidator(base).doesSomething() ? base : null;
    }

    /**
     * Creates and configures the validator.
     * 
     * @return The created validator
     * @throws ValidatorInitialisationException If something went wrong
     */
    public IValidator create() throws ValidatorInitialisationException {
        try {
            inspectFields();
            inspectPropertyMethods();
            appendParentValidations();
            appendInterfaceValidations();
            appendContentValidation();
            appendBusinessContentValidation();
            cleanUp();
            return validator;
        } catch (InstantiationException e) {
            throw new ValidatorInitialisationException(e);
        } catch (IllegalAccessException e) {
            throw new ValidatorInitialisationException(e);
        } catch (IntrospectionException e) {
            throw new ValidatorInitialisationException(e);
        }
    }

    /**
     * Inspects the fields of the given class and registers the declared validators.
     * 
     * @throws InstantiationException If something went wrong
     * @throws IllegalAccessException If something went wrong
     */
    protected void inspectFields() throws InstantiationException, IllegalAccessException {
        // inspect the fields declared in this class
        for (Field f : type.getDeclaredFields())
            inspectAnnotations(f.getName(), f);
    }

    /**
     * Inspects the properties of the given class and registers the declared validators.
     * 
     * @throws IntrospectionException If something went wrong
     * @throws InstantiationException If something went wrong
     * @throws IllegalAccessException If something went wrong
     */
    protected void inspectPropertyMethods() throws IntrospectionException, InstantiationException, IllegalAccessException {
        final PropertyDescriptor[] props = Introspector.getBeanInfo(type).getPropertyDescriptors();
        if (props == null)
            return;
        for (PropertyDescriptor d : props) {
            final Method read = d.getReadMethod();
            // skip properties not declared in the current class
            if (read == null || type != read.getDeclaringClass())
                continue;
            final String name = d.getName();
            inspectAnnotations(name, read);
            addInheritedMappingValidators(name);
        }
    }

    /**
     * Appends the validation information from the parent class.
     */
    protected void appendParentValidations() {
        final Class<?> parent = type.getSuperclass();
        // object and interfaces have not parents
        if (parent == null)
            return;
        final IValidator val = ValidatorManager.getValidator(parent);
        if (val.doesSomething() && val instanceof Validator)
            validator.appendValidatorInformation((Validator) val);
    }

    /**
     * Appends the validation information from the interface classes.
     */
    protected void appendInterfaceValidations() {
        for (Class c : type.getInterfaces()) {
            final IValidator val = ValidatorManager.getValidator(c);
            if (val.doesSomething() && val instanceof Validator)
                validator.appendValidatorInformation((Validator) val);
        }
    }

    /**
     * Appends {@link IBusinessValidator}s defined by {@link ValidateContent}/{@link ValidateBusinessContents}.
     * 
     * @throws IllegalAccessException If something went wrong
     * @throws InstantiationException If something went wrong
     */
    protected void appendContentValidation() throws InstantiationException, IllegalAccessException {
        final ValidateContent c = type.getAnnotation(ValidateContent.class);
        if (c != null)
            addContentValidator(c.value());
        final ValidateContents cs = type.getAnnotation(ValidateContents.class);
        if (cs != null)
            for (ValidateContent i : cs.value())
                addContentValidator(i.value());
    }

    /**
     * Creates a {@link IBusinessValidator} from the given type and adds it to the validator.
     * 
     * @param type The validator class
     * @throws InstantiationException If something went wrong
     * @throws IllegalAccessException If something went wrong
     */
    protected void addContentValidator(Class<? extends IBusinessValidator> type) throws InstantiationException, IllegalAccessException {
        validator.addBusinessValidator(type.newInstance());
    }

    /**
     * Appends {@link AbstractBusinessValidator}s defined by {@link ValidateBusinessContent}/{@link ValidateBusinessContents}.
     * 
     * @throws InstantiationException If something went wrong
     * @throws IllegalAccessException If something went wrong
     */
    protected void appendBusinessContentValidation() throws InstantiationException, IllegalAccessException {
        final ValidateBusinessContent c = type.getAnnotation(ValidateBusinessContent.class);
        if (c != null)
            addBusinessContentValidator(c.value(), c.resolver(), c.baseClass());
        final ValidateBusinessContents cs = type.getAnnotation(ValidateBusinessContents.class);
        if (cs != null)
            for (ValidateBusinessContent i : cs.value())
                addBusinessContentValidator(i.value(), i.resolver(), i.baseClass());
    }

    /**
     * Creates and configures a {@link AbstractBusinessValidator} from the given type and adds it to the validator.
     * 
     * @param type The validator class
     * @param resolver The class of the resolver to use
     * @param baseClass The base class to use if the resolver is a {@link MappingResolver}
     * @throws InstantiationException If something went wrong
     * @throws IllegalAccessException If something went wrong
     */
    protected void addBusinessContentValidator(Class<? extends AbstractBusinessValidator> type, Class<? extends IPropertyResolver> resolver, Class baseClass)
            throws InstantiationException, IllegalAccessException {
        final AbstractBusinessValidator v = type.newInstance();
        final IPropertyResolver res = resolver.newInstance();
        if (Object.class != baseClass && res instanceof MappingResolver)
            ((MappingResolver) res).addMappedProperties(baseClass);
        v.setResolver(res);
        validator.addBusinessValidator(v);
    }

    /**
     * Inspects the annotations of the given element and registers the declared validators.
     * 
     * @param name The property name
     * @param el The annotated element
     * @throws IllegalAccessException If something went wrong
     * @throws InstantiationException If something went wrong
     */
    protected void inspectAnnotations(String name, AnnotatedElement el) throws InstantiationException, IllegalAccessException {
        inspectMappingValidation(name, el);
        addDeepValidation(name, el);
        for (Annotation a : el.getAnnotations()) {
            final IPropertyValidator val = createValidator(a);
            if (val != null)
                validator.addPropertyValidation(name, val);
        }
    }

    /**
     * Creates a validator for the given annotation if it's a validation annotation.
     * 
     * @param a The annotation
     * @return The corresponding validator or null
     */
    protected IPropertyValidator createValidator(Annotation a) {
        final Class<? extends Annotation> type = a.annotationType();
        final ExternalAnnotationMapping extern = ValidatorManager.getMappedValidator(type);
        if (extern != null)
            return createValidator(extern, a);
        final BeanPropertyValidator intern = type.getAnnotation(BeanPropertyValidator.class);
        if (intern != null)
            return createValidator(intern.value(), a);
        return null;
    }

    /**
     * Creates a validator for the given external annotation.
     * 
     * @param extern The external mapping
     * @param a The annotation holding the configuration
     * @return The validator
     * @throws ValidatorInitialisationException If something went wrong
     */
    @SuppressWarnings("unchecked")
    protected IPropertyValidator createValidator(ExternalAnnotationMapping extern, Annotation a) {
        try {
            extern.copyConfig(a, cfg);
            cfg.setConcernedClass(type);
            final Class<? extends IPropertyValidator> type = extern.getValidatorType();
            final IPropertyValidator val = type.newInstance();
            val.init(cfg);
            return val;
        } catch (IllegalAccessException e) {
            throw new ValidatorInitialisationException(e);
        } catch (InstantiationException e) {
            throw new ValidatorInitialisationException(e);
        } catch (ExternalConfigurationException e) {
            throw new ValidatorInitialisationException(e);
        }
    }

    /**
     * Creates an configures the validator for the given annotation.
     * 
     * @param type The validator type
     * @param a The annotation holding the configuration
     * @return The validator
     * @throws ValidatorInitialisationException If something went wrong
     */
    protected IPropertyValidator createValidator(Class<? extends IPropertyValidator> type, Annotation a) throws ValidatorInitialisationException {
        try {
            cfg.clear();
            for (Method m : a.annotationType().getDeclaredMethods())
                cfg.addEntry(m.getName(), m.invoke(a));
            cfg.setConcernedClass(this.type);
            final IPropertyValidator val = type.newInstance();
            val.init(cfg);
            return val;
        } catch (IllegalAccessException e) {
            throw new ValidatorInitialisationException(e);
        } catch (InvocationTargetException e) {
            throw new ValidatorInitialisationException(e);
        } catch (InstantiationException e) {
            throw new ValidatorInitialisationException(e);
        }
    }

    /**
     * Adds a deep validation if the given element is annotated with {@link DeepValidation}.
     * 
     * @param name The property name
     * @param el The annotated element
     */
    protected void addDeepValidation(String name, AnnotatedElement el) {
        final DeepValidation deep = el.getAnnotation(DeepValidation.class);
        if (deep != null)
            validator.addDeepValidation(name, deep.message());
    }

    /**
     * Inspects mapping annotation and extracts validation relevant information.
     * 
     * @param property The property name
     * @param el The annotated element
     */
    protected void inspectMappingValidation(String property, AnnotatedElement el) {
        if (ignoreInheritedMappingValidation(property, el))
            return;
        final PathMapping path = el.getAnnotation(PathMapping.class);
        if (path != null)
            addPathMapping(property, path);
        final SimpleMapping simple = el.getAnnotation(SimpleMapping.class);
        if (simple != null)
            addSimpleMapping(property, simple);
    }

    /**
     * Checks if the given element should not inherit it's validation rules through mapping annotations.<br>
     * Ensures that a call of {@link #addInheritedMappingValidators(String)} does nothing if not desired.
     * 
     * @param name The property name
     * @param el The property element
     * @return True if the element should not inherit it's validation rules
     */
    protected boolean ignoreInheritedMappingValidation(String name, AnnotatedElement el) {
        final boolean ignore = el.isAnnotationPresent(IgnoreInheritedValidation.class) || el.isAnnotationPresent(IgnoreMapping.class);
        if (ignore)
            noDefaultMappingValidation.add(name);
        return ignore;
    }

    /**
     * Adds the necessary validators for the given property name based on the defined {@link SimpleMapping}.
     * 
     * @param srcProperty The name of the property in the source type (from which the validators were inherited)
     * @param simple The mapping annotation
     */
    protected void addSimpleMapping(String srcProperty, SimpleMapping simple) {
        noDefaultMappingValidation.add(srcProperty);
        final Class<?> srcClass = simple.srcClass();
        final Class<?> type = srcClass != void.class ? srcClass : mappedBaseType;
        if (type != null)
            inheritValidators(srcProperty, type, simple.value());
    }

    /**
     * Adds the necessary validators for the given {@link PathMapping} annotation.
     * 
     * @param property The property
     * @param mapping The mapping annotation
     */
    protected void addPathMapping(String property, PathMapping mapping) {
        noDefaultMappingValidation.add(property);
        final Class<?> lastType = mapping.lastPathType();
        if (lastType != Object.class)
            inheritValidators(property, lastType, ELHelper.getLastProperty(mapping.path()));
    }

    /**
     * Adds the validators of the default mapping of the given property.<br>
     * This is only done if no other mapping annotation exists on the specified property!
     * 
     * @param property The property
     */
    protected void addInheritedMappingValidators(String property) {
        if (mappedBaseType != null && noDefaultMappingValidation.add(property))
            inheritValidators(property, mappedBaseType, property);
    }

    /**
     * Adds all validators registered on the specified source property in the specified type to the validator list of the passed property.
     * 
     * @param dstProperty The property name in the type for which the validator is built
     * @param src The source type (from which the validators were inherited)
     * @param srcProperty The name of the property in the source type
     */
    protected void inheritValidators(String dstProperty, Class<?> src, String srcProperty) {
        final IValidator val = ValidatorManager.getValidator(src);
        if (val instanceof Validator)
            validator.appendSimpleValidators((Validator) val, srcProperty, dstProperty);
    }

    /**
     * Cleans up the helper.
     */
    protected void cleanUp() {
        noDefaultMappingValidation.clear();
    }
}